﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="KeyedXmlRecord.cs" company="" />
//
// <summary>
//   This is an 'XML record'. An XML record is defined to be a comparable representation of any XML element which has no childs,
//   or which childs are leafs - only attributes or XML elements containing no sub-elements or attributes.
//   Allows equality comparison based on a default key (primary if exists) and key-map, all given by a parenting <see cref="ParsedXmlTree" /> object.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Utilities
{
    using System;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Xml.Linq;

    /// <summary>
    /// This is an 'XML record'. An XML record is defined to be a comparable representation of any XML element which has no childs,
    /// or which childs are leafs - only attributes or XML elements containing no sub-elements or attributes.
    /// Allows equality comparison based on a default key (primary if exists) and key-map, all given by a parenting <see cref="ParsedXmlTree"/> object.
    /// </summary>
    public sealed class KeyedXmlRecord : IEquatable<KeyedXmlRecord>
    {
        /// <summary>
        /// The containing parent parsed XML tree.
        /// </summary>
        private readonly ParsedXmlTree m_containerXmlTree;

        /// <summary>
        /// Initializes a new instance of the <see cref="KeyedXmlRecord"/> class, supplying the container tree, represented element and its path.
        /// Sets the key used for comparison, by the defined key mapping and default key in the parenting container XML tree.
        /// </summary>
        /// <param name="containerXmlTree">The parenting container XML tree.</param>
        /// <param name="elementPath">The represented element path. The name of the element is contained as last in the path.</param>
        /// <param name="element">The represented XML element.</param>
        public KeyedXmlRecord(ParsedXmlTree containerXmlTree, string elementPath, XElement element)
        {
            m_containerXmlTree = containerXmlTree;
            Element = element;

            // Create the ordered list of keys (the first one is the default key - if exists):
            if (!Element.HasAttributes && !Element.HasElements)
            {
                var adjustedElementName = MiscUtils.AdjustCaption(Element.Name, containerXmlTree.IgnoreNameCase, containerXmlTree.IgnoreWhiteSpaces);

                if (Element.Parent == null || !containerXmlTree.ElementRecords.ContainsKey(Element.Parent))
                {
                    Key = adjustedElementName;
                    KeyValue = adjustedElementName.GetHashCode().ToString(CultureInfo.InvariantCulture);
                }
                else
                {
                    // The given element is a leaf in itself:
                    Key = adjustedElementName;
                    KeyValue = containerXmlTree.ElementRecords[Element.Parent].KeyValue;
                }
            }
            else
            {
                // The given element has at-least attributes or sub-elements:
                if (!string.IsNullOrEmpty(m_containerXmlTree.DefaultKeyName) && Element.Attributes(m_containerXmlTree.DefaultKeyName).Any())
                {
                    Key = MiscUtils.AdjustCaption(m_containerXmlTree.DefaultKeyName, containerXmlTree.IgnoreNameCase, containerXmlTree.IgnoreWhiteSpaces);
                }
                else
                {
                    var tempKey = m_containerXmlTree.KeyMap != null && m_containerXmlTree.KeyMap.ContainsKey(elementPath)
                        ? m_containerXmlTree.KeyMap[elementPath]
                        : null;

                    Key = tempKey == null ? null : MiscUtils.AdjustCaption(tempKey, containerXmlTree.IgnoreNameCase, containerXmlTree.IgnoreWhiteSpaces);
                }

                var tempKeyValue = Key != null && Element.Attributes(Key).Any()
                    ? Element.Attributes(Key).First().Value
                    : GetHashCode().ToString(CultureInfo.InvariantCulture);

                KeyValue = MiscUtils.AdjustCaption(tempKeyValue, containerXmlTree.IgnoreValueCase, containerXmlTree.IgnoreWhiteSpaces);
            }

            RecordPath = string.Format(
                CultureInfo.InvariantCulture,
                "{0}({1})",
                GetElementPathWithoutFirstKey(MiscUtils.AdjustCaption(elementPath, m_containerXmlTree.IgnoreNameCase, m_containerXmlTree.IgnoreWhiteSpaces)),
                KeyValue);
        }

        /// <summary>
        /// Decide whether a given XML element is considered a record.
        /// An XML record is an XML element containing attributes, or having no child elements,
        /// or having only leaf child elements.
        /// </summary>
        /// <param name="element">The element to decide if it is an XML record.</param>
        /// <returns>Whether the given element is an XML record.</returns>
        public static bool IsRecord(XElement element)
        {
            return element.HasAttributes ||
                !element.HasElements ||
                (element.HasElements && !element.Elements().Any(childElement => childElement.HasElements || childElement.HasAttributes));
        }

        /// <summary>
        /// Retrievs the XML element path without the first key stated in parenthesis within the given XML element path.
        /// </summary>
        /// <param name="elementPath">The given XML path</param>
        /// <returns>Same given path without the first key in parenthesis.</returns>
        public static string GetElementPathWithoutFirstKey(string elementPath)
        {
            int keyStartPosition = elementPath.IndexOf('(');
            int keyEndPosition = elementPath.IndexOf(')');

            return keyStartPosition == -1 ? elementPath : elementPath.Substring(0, keyStartPosition) + elementPath.Substring(keyEndPosition + 1);
        }

        /// <summary>
        /// Retrieves the first key in the given element path. The key is identified by the parenthesis surrounding it.
        /// </summary>
        /// <param name="elementPath">The XML element path to extract the key from.</param>
        /// <returns>The required key.</returns>
        public static string GetKeyFromElementPath(string elementPath)
        {
            int keyStartPosition = elementPath.IndexOf('(');
            int keyEndPosition = elementPath.IndexOf(')');

            return keyStartPosition == -1 ? elementPath : elementPath.Substring(keyStartPosition + 1, keyEndPosition - keyStartPosition - 1);
        }

        /// <summary>
        /// Gets the calculated key used to identify and match this <see cref="KeyedXmlRecord"/> object.
        /// </summary>
        public XName Key { get; private set; }

        /// <summary>
        /// Gets the value held by the decided key of this record, or, if none decided - the calculated hash-value for this record.
        /// </summary>
        public string KeyValue { get; private set; }

        /// <summary>
        /// Gets the path of the XML element leaf containing this record.
        /// </summary>
        public string RecordPath { get; private set; }

        /// <summary>
        /// The XML element represented by this record.
        /// </summary>
        public XElement Element { get; private set; }

        #region Overrides of object

        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            var keyedRecord = obj as KeyedXmlRecord;
            if (keyedRecord == null)
            {
                return Equals(this, obj);
            }

            return Equals(keyedRecord);
        }

        public override int GetHashCode()
        {
            var ignoreNameCase = m_containerXmlTree.IgnoreNameCase;
            var ignoreValueCase = m_containerXmlTree.IgnoreValueCase;
            var ignoreWhiteSpaces = m_containerXmlTree.IgnoreWhiteSpaces;
            var adjustedKey = Key == null ? string.Empty : MiscUtils.AdjustCaption(Key.ToString(), ignoreNameCase, ignoreWhiteSpaces);
            var adjustedElementHashCode = MiscUtils.AdjustCaption(MiscUtils.GetElementSingleValue(Element), ignoreValueCase, ignoreWhiteSpaces).GetHashCode();

            // Take into account the value of this XML element and attributes:
            var result = Element.Attributes()
                .Where(contained => !contained.Name.ToString().Equals(adjustedKey))
                .Aggregate(
                    adjustedElementHashCode,
                    (current, attribute) => current ^
                        MiscUtils.AdjustCaption(attribute.Name.ToString(), ignoreNameCase, ignoreWhiteSpaces).GetHashCode() ^
                        MiscUtils.AdjustCaption(attribute.Value, ignoreValueCase, ignoreWhiteSpaces).GetHashCode());

            return result;
        }

        public override string ToString()
        {
            var result = new StringBuilder();
            var cleanElementPath = Regex.Replace(RecordPath, @"{[^\{\}]+}", "{}");

            result.AppendFormat(
                CultureInfo.InvariantCulture,
                "[{0}] path: {1} ({2})",
                Key,
                cleanElementPath,
                Element.HasElements ? string.Empty : Element.Value);
            result.AppendLine();
            foreach (var attribute in Element.Attributes())
            {
                result.AppendFormat(CultureInfo.InvariantCulture, " - {0}={1}", attribute.Name, attribute.Value);
                result.AppendLine();
            }

            return result.ToString();
        }

        #endregion

        #region Implementation of IEquatable<KeyedXmlRecord>

        public bool Equals(KeyedXmlRecord other)
        {
            return KeyValue == other.KeyValue;
        }

        #endregion
    }
}